package pub.gll.plugin.asm.call;


import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import java.util.Objects;

import pub.gll.libaptannotation.Replace;
import pub.gll.plugin.asm.model.Source;
import pub.gll.plugin.asm.model.Target;
import pub.gll.plugin.util.CallInfoUtils;
import pub.gll.plugin.util.ClassUtil;
import pub.gll.plugin.util.CloseUtil;
import pub.gll.plugin.util.Log;
import pub.gll.plugin.util.StringUtil;

/**
 * {@link pub.gll.libaptannotation.Replace}
 *
 * @author yuqiang
 */
public class ReplaceCallMethodVisitor extends AbstractCallMethodVisitor {

    public ReplaceCallMethodVisitor(MethodVisitor mv, Source source) {
        super(mv, source);
    }


    @Override
    protected void innerCallMethodInst() {
        if (needCallOriginMethod()) {
            return;
        }
        checkParamIsValid();
        configSourceInfo();
        Log.i(getTag(), source.getClassName() + "." + source.getMethodName() + "." + source.getMethodDesc() + String.format("%s.%s.%s replace by %s.%s.%s",
                callMethodInst.owner, callMethodInst.name, callMethodInst.desc, target.getClassName(), target.getMethodName(), target.getMethodDesc()));
        mv.visitMethodInsn(INVOKESTATIC, target.getClassName(), target.getMethodName(), target.getMethodDesc(), false);
    }

    private void configSourceInfo() {
        if (target.isNeedSourceInfo()) {
            String sourceInfo = source.getClassName() + " : " + source.getMethodName() + " : " + source.getMethodDesc();
            mv.visitLdcInsn(sourceInfo);
        }
    }

    private void checkParamIsValid() {
        int sourceParamCount = Type.getArgumentTypes(callMethodInst.desc).length;
        int targetParamCount = Type.getArgumentTypes(target.getMethodDesc()).length;
        boolean needSourceInfo = target.isNeedSourceInfo();
        int targetRealCount = targetParamCount - (needSourceInfo ? 1 : 0);
        checkParamCount(sourceParamCount, targetRealCount);
        checkParamType();
    }

    private boolean checkParamType() {
        int targetStartCount = 0;
        Type[] sourceParamType = Type.getArgumentTypes(callMethodInst.desc);
        Type[] targetParamTypes = Type.getArgumentTypes(target.getMethodDesc());

        if (callMethodInst.opcode == INVOKEVIRTUAL || callMethodInst.opcode == INVOKEINTERFACE || callMethodInst.opcode == INVOKESPECIAL) {
            if (!callMethodInst.name.contains("<init>")) {//构造方法第一个参数不校验<init>代表构造方法
                String ownerClassName = callMethodInst.owner;
                Type type = targetParamTypes[0];
                String firstParamClassName = type.getClassName();
                if (!ClassUtil.isSuper(ownerClassName, firstParamClassName)) {
                    CloseUtil.exit(firstParamClassName + " is a subclass of " + ownerClassName + " or itself! ");
                }
                targetStartCount++;
            }
        }
        try {
            for (Type type : sourceParamType) {
                String sourceParamTypeName = type.getClassName();
                String targetParamTypeName = targetParamTypes[targetStartCount].getClassName();
                if (!sourceParamTypeName.equals(targetParamTypeName)) {
                    CloseUtil.exit("callMethodInst: " + callMethodInst + ", target: " + target.getMethodDesc() + "param type not equals " + sourceParamTypeName + " != " + targetParamTypeName);
                }
                targetStartCount++;
            }
        } catch (Exception e) {
            CloseUtil.exit("sourceParamType check error!");
        }
        if (target.isNeedSourceInfo()) {
            Type sourceInfoType = targetParamTypes[targetStartCount];
            String sourceName = "java.lang.String";
            if (!sourceName.equals(sourceInfoType.getClassName())) {
                CloseUtil.exit("callMethodInst: " + callMethodInst + ", target: " + target.getMethodDesc() + " source info 只支持String类型");
            }
        }
        return true;
    }

    private void checkParamCount(int sourceCount, int targetCount) {
        switch (callMethodInst.opcode) {
            case INVOKESTATIC:
                if (sourceCount != targetCount) {
                    CloseUtil.exit("illegal param count, source: " + source + ", target:" + target);
                }
                break;
            case INVOKEVIRTUAL:
                if (sourceCount + 1 != targetCount) {
                    CloseUtil.exit("illegal param count, source: " + source + ", target:" + target);
                }
                break;
            default:
                break;

        }
    }

    private boolean needCallOriginMethod() {
        if (recursiveReplace(target) || isSameMethodReplace(callMethodInst)) {
            mv.visitMethodInsn(callMethodInst.opcode, callMethodInst.owner, callMethodInst.name, callMethodInst.desc, callMethodInst.itf);
            Log.e(getTag(), "recursive call occurs stackOverflowException discard!");
            return true;
        }
        return false;
    }

    /**
     * {@link pub.gll.plugin.ClassMethodKnife.KnifeClassAdapter#visitAnnotation(String, boolean)}
     * 扫描类掉时候已经排除掉
     * 消除递归替换导致的 StackOverflowException
     * Kotlin注解需要调用该方法
     * Source.className = *.$Companion
     * Target.className = *
     * so source.className.equals(target.className + "$Companion")
     *
     * @param target 目标指令信息
     * @return true   discard
     * false
     */
    private boolean recursiveReplace(Target target) {
        return this.source != null && target != null &&
                StringUtil.replaceSlash2Dot(this.source.getClassName())
                        .equals(StringUtil.replaceSlash2Dot(target.getClassName()) + "$Companion") &&
                Objects.equals(this.source.getMethodName(), target.getMethodName()) &&
                Objects.equals(this.source.getMethodDesc(), target.getMethodDesc());
    }

    /**
     * 解决同一个方法下， 在Replace替换下造成递归导致的 StackOverflowException
     *
     * @param callMethodInst 原始方法信息
     * @return
     */
    private boolean isSameMethodReplace(CallMethodInst callMethodInst) {
        if (source == null || callMethodInst == null) {
            return false;
        }
        if (!Objects.equals(this.source.getMethodName(), callMethodInst.name)) {
            return false;
        }
        return ClassUtil.isSuper(source.getClassName(), callMethodInst.owner) &&
                Objects.equals(this.source.getMethodName(), callMethodInst.name) &&
                Objects.equals(this.source.getMethodDesc(), callMethodInst.desc);

    }

    @Override
    protected Class getAnnotationClass() {
        return Replace.class;
    }

    @Override
    protected String getTag() {
        return "Replace";
    }

    @Override
    protected boolean runCallMethodInst() {
        if (!targetListValid()) {
            return false;
        }
        return CallInfoUtils.isInRunMethodInstWhiteList(targetList.get(0), source);
    }
}
